;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; \brief	汇编延时函数，供C语言调用
; \remark	File format: UTF-8，源文件使用UTF-8中文编码
; \author	将狼才鲸
; \date		2024-07-22
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

$NOMOD51	; 不使用Keil默认隐含的8051通用寄存器定义，而是显式的包含寄存器定义头文件

;; 定义本汇编文件的代码段，不定义的话放到位置不固定，有可能上电RESET后直接就跑到这里来了 ;
;  如果该汇编文件会被别的汇编文件直接包含，则不用定义，同时文件尾也不用加END
NAME	DELAY_ASM
DELAY_ASM  SEGMENT  CODE	; SEGMENT CODE是定义代码段
RSEG  DELAY_ASM

; 8051汇编指令及每条指令所消耗的时钟数详见：
; https://gitee.com/langcai1943/8051-from-boot-to-application/blob/develop/02_doc/01_8051寄存器、指令集、伪指令和关键字介绍.md

; 当前工程中配置的晶振是Keil默认的24MHz，时钟周期41.67ns，状态周期是2倍时钟周期即83.3333ns，
; 机器周期是6倍状态周期即500ns，一个机器周期记为1T，一条指令周期为1~4T，具体哪条指令占多少T需要查阅上面文档

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 函数声明 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
PUBLIC _delay_us_10x	; 类似于C语言头文件中的 extern void delay_us_10x(uint8 cnt); 让别的文件中能调用此函数
PUBLIC _delay_ms

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 宏定义 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
INS_SET_10US_NUM	EQU 20	; 10us占20个机器周期；当前工程中配置的晶振是Keil默认的24MHz，时钟周期41.67ns，机器周期500ns；

;; 宏定义函数
DELAY_1US  MACRO
	NOP	; 1T，500ns
	NOP
ENDM

DELAY_2US  MACRO
	DELAY_1US
	DELAY_1US
ENDM

DELAY_4US  MACRO
	DELAY_2US
	DELAY_2US
ENDM

DELAY_8US  MACRO
	DELAY_4US
	DELAY_4US
ENDM

DELAY_16US  MACRO
	DELAY_8US
	DELAY_8US
ENDM

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 接口函数 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
; \brief	以10us为单位的延时函数（必须延时20us及以上，否则请直接使用NOP进行延时！）
; \details	1. 使用小写标号（函数名），在C语言中调用该函数时让函数名与C语言标准更统一；
;			2. 传入的参数需要 >= 2，否则会有异常的长时间延时，也就是说至少延时2个10us；
; \note		注意：要使用汇编实现精准延时的话，需要在调用延时前关闭所有中断，调用后再恢复中断！
; \remark	C语言调用此汇编函数时使用 delay_us_10x(100); // 延时1000us
; \param	0xR7	uint8，延时多少个10us，取值为 2 ~ 255
; \return	无
;;
_delay_us_10x:
	; 外部调用此函数时如果有给R7赋值则会额外消耗1T（已优化）
	; 外部调用此函数的LCALL或ACALL会额外占2T（已优化）
	DEC		R7					; 1T，将10us的次数直接减1；用于补齐10us，抵消额外的消耗

	;; 抵消额外消耗的6T，再补14T凑成10us；
	;  \note	如果晶振有变化，则这里的补时也要调整，或者修改得和 INS_SET_10US_NUM 相关
	DELAY_4US					; 这里是宏定义函数
	DELAY_2US
	DELAY_1US

	;; 延时10us
DELAY_US_LOOP2:
	MOV		R6, #((INS_SET_10US_NUM - 4) / 2)	; 1T
	NOP											; 1T，空指令，什么也不做；为了补齐MOV指令的1T时间到2T
DELAY_US_LOOP1:
	DJNZ	R6, DELAY_US_LOOP1	; 2T

	DJNZ	R7, DELAY_US_LOOP2	; 2T，延时多少个10us

	RET	; 从函数中返回；返回额外占2T（已优化）

;;
; \brief	ms延时函数
; \details	1. 传入的参数必须 >= 1，即至少延时1ms，如果为0则会导致超长时间延时
;			2. 当低字节为0x01时，本延时函数少了2T也就是1us；
;			3. 当低字节为0x00时，本延时函数多了4T也就是2us；
; \note		注意：要使用汇编实现精准延时的话，需要在调用延时前关闭所有中断，调用后再恢复中断！
; \remark	C语言调用此汇编函数时使用 delay_ms(1000); // 延时1000ms
; \param	0xR6R7-->0xR4R5	uint16，延时多少ms；取值范围为0x0002~0xFEFF（注意不是0xFFFF!）
; \return	无
;;
_delay_ms:
	; 外部调用此函数时如果有给R6R7赋值则会额外消耗2T（已优化）
	; 外部调用此函数的LCALL或ACALL会额外占2T（已优化）

	; 0xR4R5 = 0xR6R7
	MOV		A, R7	; 1T，因为之后R7还需作为子函数的参数，所以将0xR6R7先移动到0xR4R5
	MOV		R5, A	; 1T，不能直接MOV R5, R7; 没有这种指令，两个内部寄存器之间不能直接赋值
	MOV		A, R6	; 1T，下面调用us延时函数时没有进行压栈弹栈操作，us延时里面用了R6作为临时变量，所以这里也要避开
	MOV		R4, A	; 1T

	INC		R4		; 1T，R4自增1；如果R4本来为0，则本函数最后的R4减1不为0跳转会有bug；因此高位的取值只能是0x00~0xFE，不能取到0xFF

	;; 函数进入和函数返回时的额外消耗是24T（其中有2T是R4自增一导致的），12us，此处将其补齐到1ms，
	;  还需额外补980us + 8us；并后续将0xR4R5减1，也就是减去这补齐的1ms
	DELAY_8US
	MOV		R7, #98			; 1T已抵消，_delay_us_10x函数的参数，980us
	LCALL	_delay_us_10x	; 2T已抵消

	;; 将0xR4R5减1，用于时间凑整补齐
	;  执行所有INC或DEC自增自减、所有ADD加法指令时都不会产生PSW寄存器的CY借位；
	;  只有ADDC和SUBB指令才带进位或借位，进位和借位都是PSW寄存器的CY位；
	;  SUBB不能用R0~R7去减别的数，只能用累加器A去减
	;; 低字节减1
	MOV		A, R5	; 1T
	CLR		C		; 1T，CLR C和CLR CY是一样的效果，C是指令集里面特定的用法，CY是头文件中BIT伪指令定义的位
	SUBB	A, #1	; 1T
	MOV		R5, A	; 1T，减完后放回R5
	JNC		DELAY_MS_HIGH_BYTE_IGNORE_DEC	; 2T，如果CY进位为0则跳转
	;; 类似跳转指令还有：JNZ累加器为1跳转，JZ累加器为0跳转，JNC进位为0跳转，JC进位为1跳转，
	;  JNB比特为0跳转，JB比特为1跳转，JBC比特为1跳转并清零（前面其他指令都不会自动清零）

	;; 如果有借位则高字节减1（因为有减1的步骤，所以传入的参数不能为0x0001）
	;  当低字节为0x00时，此处会执行，则会多了4T也就是2us
	;                               ;=====================;
	CLR		C		; 1T，CLR C和CLR CY是一样的效果，C是指令集里面特定的用法，CY是头文件中BIT伪指令定义的位
	MOV		A, R4	; 1T
	SUBB	A, #1	; 1T，SUBB使用前要清零进位CY，否则借位存在的话会多减去1
	MOV		R4, A	; 1T，减完后放回R4

DELAY_MS_HIGH_BYTE_IGNORE_DEC:

	;; 处理传入的参数低字节为0的情况
	MOV		A, R5					; 1T
	JZ		DELAY_MS_HIGH_BYTE_JMP	; 2T，累加器A为0则跳转
	LJMP	DELAY_MS_BYTE_LOOP_LOW	; 2T，当低字节减完1后为0时，此处会少了2T也就是1us，少的这1us没有进行优化
	;                                                          ;=====================;
DELAY_MS_BYTE_LOOP_HIGH:
	; 每次高字节循环256ms时的额外消耗是4T，2us；此处将其补齐到10us，后面再调用990us
	DELAY_8US

	;; 延时1ms；高字节减1，低字节从256变成255时，将少的那一次补上
	MOV		R7, #99			; 1T已抵消，_delay_us_10x函数的参数
	LCALL	_delay_us_10x	; 2T已抵消

	MOV		R5, #0FFH		; 2T，R5 = 255
	;; 延时1 ~ 255ms
DELAY_MS_BYTE_LOOP_LOW:
	; 每次低字节循环1ms时的额外消耗是2T，1us，此处将其补齐到10us，后面再调用990us
	DELAY_8US
	DELAY_1US

	;; 延时1ms
	MOV		R7, #99			; 1T已抵消，_delay_us_10x函数的参数
	LCALL	_delay_us_10x	; 2T已抵消

	DJNZ	R5, DELAY_MS_BYTE_LOOP_LOW	; 2T，R5寄存器内的数据减1，不为0则跳转；延时多少个ms

DELAY_MS_HIGH_BYTE_JMP:
	DJNZ	R4, DELAY_MS_BYTE_LOOP_HIGH	; 2T，处理高字节，延时多少个256ms；之前有加1，函数调用时这里额外多了2T（已优化）

	RET	; 2T，返回额外占2T（已优化）

END	; 源文件结束；如果该汇编文件会被别的汇编文件直接包含，则不用加END
